import java.net.*;
import java.io.*;
import java.util.*;

public class ChatClient {
  private ChatClientRecord clientRec;
  private ArrayList<ChatClientRecord> chatGroup;
  private HashMap<String, ChatConnection> chatConnections;
  
  public ChatClient(String serverName, int serverPort, int clientPort) {
    // Initialize chatGroup to zero members
    chatGroup = new ArrayList<ChatClientRecord>();

    // Prompt user for name
    Scanner scan = new Scanner(System.in);
    System.out.println("Please input your name:");
    String clientName = scan.nextLine();

    Socket sock = null;
    ObjectOutputStream out = null;
    ObjectInputStream in = null;

    // Start listening for client connections
    TCPThread tcp = null;
    try {
      tcp = new TCPThread(clientPort);
      tcp.start();
    } catch (IOException e) {
      System.out.println("Client cannot listen on port " + clientPort);
      System.exit(1);
    }

    // Attempt to connect to server
    try {
      sock = new Socket(serverName, serverPort);
      out = new ObjectOutputStream(sock.getOutputStream());
      in = new ObjectInputStream(sock.getInputStream());
    } catch (IOException e) {
      System.err.println("Could not connect to server");
      System.exit(1);
    }

    // Send registration request
    try{
    this.clientRec = new ChatClientRecord(clientName, InetAddress.getLocalHost().getHostName(), tcp.getPort());
    }catch(UnknownHostException e){ System.err.println("Error:"+e.getMessage()); }
    ChatMessage regRequest = new ChatMessage(ChatMessage.REG_ATTEMPT, null, null, clientRec);
    try {
      out.writeObject(regRequest);
    } catch (IOException e) {
      System.err.println("Could not write registration request.");
      System.exit(1);
    }

    // If registration is successful, start listening for other client connections and
    // user attempts to connect to other clients
    try {
      ChatMessage regResponse = (ChatMessage)in.readObject();
      if (regResponse.type == ChatMessage.REG_SUCCESS) {
        System.out.println("Registration success!"); 
        // Initialize ChatConnection list
        chatConnections = new HashMap<String, ChatConnection>();
        // Start sending hearbeats
        TimerTask heartbeatSender = new HeartbeatSender(serverName, serverPort); 
        Timer heartbeatTimer = new Timer();
        heartbeatTimer.schedule(heartbeatSender, 30*1000, 30*1000);
        // Read commands from stdin
        Scanner stdin = new Scanner(System.in);
        String line = null;
        while (true) {
          line = stdin.nextLine();
          String[] cmdParts = line.split(" ");
          String cmd = cmdParts[0];
          // Quit the chat client
          if (cmd.equals("exit")) {
            break;
          }
          // Get a list of connected clients from the server
          if (cmd.equals("list")) {
            Socket serverSock = new Socket(serverName, serverPort);
            ObjectOutputStream serverOut = new ObjectOutputStream(serverSock.getOutputStream());
            ObjectInputStream serverIn = new ObjectInputStream(serverSock.getInputStream());
            ChatMessage msg = new ChatMessage(ChatMessage.GET_LIST, null, null, null);
            serverOut.writeObject(msg);
            ChatMessage response = (ChatMessage)serverIn.readObject();
            chatGroup = response.groupList;
            System.out.println("Connected clients:");
            for (ChatClientRecord rec : chatGroup) {
              if (rec.name.equals(this.clientRec.name))
                System.out.println(rec.name + "(me) ");
              else
                System.out.println(rec.name + " ");
            }
          }
          // Initiate a chat connection with a user
          if (cmd.equals("connect")) {
            if (cmdParts.length != 2) {
              System.out.println("Usage: connect user");
            } else {
              String targetName = cmdParts[1];
              for (int i = 2; i < cmdParts.length; i++)
                targetName += (" " + cmdParts[i]);             
              if (chatConnections.containsKey(targetName)) {
                System.out.println("You are already connected to " + targetName);
              } else {
                // Find target user's client record
                boolean foundClient = false;
                for (ChatClientRecord rec : chatGroup) {
                  if (rec.name.equals(targetName)) {
                    foundClient = true;
                    try {
                      System.out.println("ip:"+rec.ip);
                      Socket targetSock = new Socket(rec.ip, rec.port);
                      ChatConnection conn = new ChatConnection(targetSock, ChatConnection.ORIGINATOR);
                      conn.targetClientRec = rec;
                      conn.start();
                      chatConnections.put(targetName, conn);
                      System.out.println("Connected successfully to " + targetName);
                      break;
                    } catch (IOException e) {
                      System.err.println(e.getMessage());
                      System.err.println("Could not connect to " + targetName);
                    }
                  }
                }
                if (!foundClient) {
                  System.out.println("Could not find information for " + targetName + ". Try running list first.");
                }
              }
            }
          }
          // Send IM to target user
          if (cmd.equals("chat")) {
            if (cmdParts.length < 3) {
              System.out.println("Usage: chat user message");
            } else {
              String targetName = cmdParts[1];
              String message = cmdParts[2];
              for (int i = 3; i < cmdParts.length; i++)
                message += (" " + cmdParts[i]);             
              if (!chatConnections.containsKey(targetName)) {
                System.out.println("You need to connect to " + targetName + " first");
              } else {
                ChatConnection conn = chatConnections.get(targetName);
                conn.sendMessage(new ChatMessage(ChatMessage.IM, message, null, null)); 
              }
            }
          }
          // print available commands
          if (cmd.equals("help")) {
            System.out.println("Commands:\n\tlist\n\tconnect <user>\n\tchat <user> <message>\n\texit");
          }
        }
        Set<String> connectionNames = chatConnections.keySet();
        for (String name : connectionNames) {
          System.out.println("Closing chat connection with " + name);
          chatConnections.get(name).close();
        }
        System.exit(1);
      } else if (regResponse.type == ChatMessage.REG_FAIL) {
        System.err.println("Registration failure."); 
        System.exit(1);
      }
    } catch (IOException e) {
      System.err.println("Something went wrong on the network");
      System.exit(1);
    } catch (ClassNotFoundException e) {
      System.err.println("Could not translate registration response to a class");
      System.exit(1);
    }
  }

  private class HeartbeatSender extends TimerTask {
    private String serverName;
    private int serverPort;

    public HeartbeatSender(String serverName, int serverPort) {
      this.serverName = serverName;
      this.serverPort = serverPort;
    }

    // Send heartbeat to server
    public void run() {
      try {
        Socket sock = new Socket(serverName, serverPort);    
        ObjectOutputStream out = new ObjectOutputStream(sock.getOutputStream());
        out.writeObject(new ChatMessage(ChatMessage.HEARTBEAT, null, null, clientRec));
      } catch (UnknownHostException e) {
        System.err.println("Could not connect to " + serverName + ":" + serverPort);
        System.exit(1);
      } catch (IOException e) {
        System.err.println("Could not send heartbeat");
        System.exit(1);
      }
    }
  }

  private class TCPThread extends Thread {
    private ServerSocket serverSocket;

    public TCPThread(int port) throws IOException {
      serverSocket = new ServerSocket(port);
      System.out.println("ip:"+serverSocket.toString());
      System.out.println("Client now accepting connections on port " + port);
    }

    public String getIP() {
      return serverSocket.getInetAddress().getHostName();
    }

    public int getPort() {
      return serverSocket.getLocalPort();
    }

    public void run() {
      while (true) {
        // Accept a new connection
        Socket clientSocket = null;
        try {
          clientSocket = serverSocket.accept();
          ChatConnection conn = new ChatConnection(clientSocket, ChatConnection.TARGET);
          conn.start();
        } catch (IOException e) {
          System.err.println("Accept failed (IOException)");
        }
      }
    }
  }

  private class ChatConnection extends Thread {
    private Socket sock;
    private MessageSender sender;
    private MessageReceiver receiver;
    private int connType;
    private ObjectOutputStream out;
    private ObjectInputStream in;
    public ChatClientRecord targetClientRec;
    public static final int ORIGINATOR = 0;
    public static final int TARGET = 1;

    public ChatConnection(Socket sock, int connType) {
      this.sock = sock;
      this.connType = connType;

      try {
        out = new ObjectOutputStream(sock.getOutputStream());
        in = new ObjectInputStream(sock.getInputStream());
        
        // Enable sending of messages from other client
        sender = new MessageSender(out);
        sender.start();

        // Enable receiving of objects
        receiver = new MessageReceiver(in);
        receiver.start();

        // Expect client record as first packet sent
        if (connType == ORIGINATOR) {
          ChatMessage msg1 = new ChatMessage(ChatMessage.CLIENT_REC, "clientRec", null, clientRec);
          sendMessage(msg1);
        }
      } catch (IOException e) {
        System.err.println("IOException on socket");
        System.exit(1);
      } 
    }

    public void close() {
      try {
        sender.stopRunning = true;
        receiver.stopRunning = true;
        out.close();
        in.close();
        sock.close();
      } catch (IOException e) {
        System.out.println("Could not close ChatConnection");
      }
    }

    public void sendMessage(ChatMessage msg) {
      sender.sendMessage(msg);
    }

    private void setTargetClientRec(ChatClientRecord rec) {
      this.targetClientRec = rec;
      chatConnections.put(rec.name, this);
    }

    private class MessageSender extends Thread {
      private ObjectOutputStream out;
      private LinkedList<ChatMessage> msgQueue;
      public volatile boolean stopRunning = false;

      public MessageSender(ObjectOutputStream out) {
        this.out = out;
        this.msgQueue = new LinkedList<ChatMessage>();
      }

      public void sendMessage(ChatMessage msg) {
        synchronized (msgQueue) {
          msgQueue.addLast(msg);
        }
      }

      // Send any messages in the message queue
      public void run() {
        while (!stopRunning) {
          synchronized (msgQueue) {
            if (msgQueue.size() > 0) {
              ChatMessage msg = msgQueue.removeFirst();
              try {
                out.writeObject(msg);
              } catch (IOException e) {
                System.err.println("Could not write message to socket");
                System.exit(1);
              }
            }
          }
        } 
      }
    }

    private class MessageReceiver extends Thread {
      private ObjectInputStream in;
      public volatile boolean stopRunning = false;

      public MessageReceiver(ObjectInputStream in) {
        this.in = in;
      }

      // Constantly check for new messages
      public void run() {
        while (!stopRunning) {
          try {
            ChatMessage msg = (ChatMessage)in.readObject();
            if (msg.type == ChatMessage.IM) {
              System.out.println(targetClientRec.name + "> " + msg.payload);
            } else if (msg.type == ChatMessage.CLIENT_REC) {
              System.out.println("Started chat session with " + msg.clientRec.name);
              setTargetClientRec(msg.clientRec);
            }
          } catch (ClassNotFoundException e) {
            System.err.println("ChatMessage class not found");
          } catch (IOException e) {
            System.err.println("Chat connection with " + targetClientRec.name + " closed");
            chatConnections.remove(targetClientRec.name);
            stopRunning = true;
          }
        }
      }
    }
  }

  public static void main(String[] args) {
    String serverName;
    int serverPort;
    int clientPort;
    if (args.length == 3) {
      serverName = args[0];
      serverPort = Integer.parseInt(args[1]);
      clientPort = Integer.parseInt(args[2]);
      new ChatClient(serverName, serverPort, clientPort);
    } else {
      System.out.println("Usage: java ChatClient hostName hostPort clientPort");
    }
  }
}
